﻿#Requires AutoHotkey v2.0
; 将以绝对路径导入的文件复制到 ./lib 中
; 对于绝对路径导入的文件，需要将其依赖也导入
#SingleInstance Force

#Include _lib\Path.ahk

libName := '_lib', eof := '`r`n'

g := ResolveGui()
g.Show()

; =============
libPath := '' ; lib 的绝对路径，选择时确定
included := Map() ; 避免重复导入，同时辅助导入文件

statisic := {
  external: 0,
}

filters := [RemoveComment]

includeRE := 'i)^#include\s+([^;\s]+)' ; 匹配导入语句，只支持导入脚本，而其他导入类型**不适用**

; 参数：
; 文件实际路径 | 写入路径 | 父脚本是否为外部脚本
Resolve(_path, _targetPath, _external := false) {
  if !FileExist(_path)
    throw Error('bad filepath: ' _path ' type: ' _external)
  if included.Has(StrLower(_path)) ; 避免重复导入
    return
  included.Set(StrLower(_path), _targetPath)  ; 实际的 : 转换的
  if _external {
    statisic.external++
    g.Log('[E]Mapping: ' _path ' => ' _targetPath)
  } else {
    g.Log('[I]Mapping: ' _path ' => ' _targetPath)
  }

  ; 开始处理
  f := FileOpen(_path, 'r', 'utf-8'), r := ''
  while !f.AtEOF {
    if not l := f.ReadLine() { ; 空行
      r .= eof
      continue
    }

    /* 在此运行过滤器 */
    ; if _external {
    ;   _Trim_(&l)
    ; }

    curDir := Path.Dir(_path)

    if RegExMatch(l, includeRE, &match) {
      ip := Trim(match[1], '[`'|"]') ; 去除可能的引号
      ; 将绝对路径约定为外部脚本；
      ; 如果以绝对路径导入项目中的脚本，将导致该脚本被添加到 lib 目录中（并不会影响运行）
      if Path.IsAbsolute(ip) {

        np := Path.Join(libPath, n := Path.Parse(ip).name) ; 转换为新路径，表示要将内容写到此处
        if _external {
          r .= '#Include ' Path.Relative(Path.Dir(_targetPath), np) eof ; 新的引用路径
        } else {
          r .= '#Include ' Path.Relative(curDir, np) eof
        }

        Resolve(ip, np, true) ; 递归处理
        continue
      } else { ; 相对路径

        np := Path.Join(curDir, ip) ; 计算绝对路径

        ; 如果以 .. 开头，在外部脚本中需要特殊处理；
        ; 而项目脚本不许要，正常处理即可。
        if SubStr(ip, 1, 2) = '..' {

          rp := Path.Join(curDir, ip) ; 计算实际路径

          if _external { ; 如果是外部脚本的相对路径

            if included.Has(_ := StrLower(rp)) { ; 已经包含
              r .= '#Include ' Path.Relative(Path.Dir(_targetPath), _ := included.Get(_)) eof
              Resolve(rp, _, true)
            } else { ; 当作绝对路径处理
              np := Path.Join(libPath, n := Path.Parse(rp).name)
              r .= '#Include ' Path.Relative(Path.Dir(_targetPath), np) eof
              Resolve(rp, np, true)
            }
            continue
          }
        }
        ; ./
        r .= '#Include ' Path.Normalize(ip) eof
        targetDir := Path.Parse(_targetPath).dir
        CreateDirIfNotExist targetDir
        Resolve(np, Path.Join(targetDir, ip), _external)
        continue
      }
    }
    r .= l eof
  }
  f.close()
  f := FileOpen(_targetPath, 'w', 'utf-8') ; 输出
  f.Write(r)
  f.Close()
  return

  _Trim_(_l) {
    for fn in filters
      fn(&_l)
    return _l ? _l '`r`n' : ''
  }
}

CreateDirIfNotExist(dirPath) {
  if !DirExist(dirPath) {
    DirCreate(dirPath)
  }
}

RemoveComment(&line) {
  copy := line
  if copy ~= '^\s*;.*$'
    copy := ''
  else if RegExMatch(copy, '(.*?)\s+;.*?$', &match)
    copy := match[1]
  line := copy
}

class ResolveGui extends Gui {

  f := ''

  __New() {
    super.__New()
    this.SetFont('s14', 'Consolas')
    this.AddText('', 'A simple tool for resolve absolute included ahk scripts.')
    this.AddEdit('ve w1400 h600 ReadOnly')
    this.AddButton('yp section w80', 'Select').OnEvent('click', (*) => this.SelectFile())
    this.AddButton('w80', 'Resolve').OnEvent('Click', (*) => this.Resolve())
    this.OnEvent('DropFiles', (g, ctrl, files, *) => Resolve(files[1], libPath))
  }

  SelectFile(*) {
    f := FileSelect(3, , 'Select a script file', 'Ahk root file(*.ahk)')
    if f {
      if this.f
        this.Log('Entry file has been changed: ' f)
      else
        this.Log('Entry file has been selected: ' f)
      this.Log('Make sure to BACKUP your files before resolving.')
      this.f := f
    }
  }

  Resolve() {
    if !this.f {
      this.Log('Select a file first.')
      return
    }
    global libPath := Path.Join(Path.Dir(this.f), libName)
    CreateDirIfNotExist libPath
    this.Log('==========START==========')
    this.Log('Resolving file: ' this.f)
    Resolve(this.f, this.f)
    this.Log('===========END===========')
    this.Log('========STATISTIC========')
    this.Log('External scripts: ' statisic.external)
    this.Log('Internal scripts: ' included.Count - statisic.external)
    this.Log('===========END===========')
    this.f := ''
  }

  Log(t) => this['e'].Value .= '>> ' t '`n'
}
